/* Copyright 2009
 *
 * This program and the accompanying materials
 * are made available under the terms of the
 * Eclipse Public License v1.0 which accompanies
 * this distribution, and is available at
 *
 * 		http://www.eclipse.org/legal/epl-v10.html
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
 * OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * Contributors:
 * 	   IBM Corporation - initial API and implementation for JDT/DLTK
 *     Sean W. Quinn - initial adoption for use with PHP from various sources.
 */
package org.eclipse.php.internal.ui.preferences.formatter.profile;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Observable;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.dltk.ui.formatter.IProfile;
import org.eclipse.dltk.ui.formatter.IProfileManager;
import org.eclipse.php.internal.ui.preferences.PreferencesAccess;
import org.eclipse.php.internal.ui.preferences.formatter.KeySet;
import org.eclipse.php.internal.ui.preferences.formatter.PHPFormatterMessages;
import org.eclipse.php.internal.ui.util.Messages;
import org.eclipse.php.ui.PHPUiHelper;
import org.osgi.service.prefs.BackingStoreException;

/**
 * The model for the set of profiles which are available in the workbench.
 */
@SuppressWarnings("restriction")
public abstract class PHPProfileManager extends Observable
  implements IProfileManager {

  /**
   * The possible events for observers listening to this class.
   */
  public final static int SELECTION_CHANGED_EVENT = 1;
  public final static int PROFILE_DELETED_EVENT = 2;
  public final static int PROFILE_RENAMED_EVENT = 3;
  public final static int PROFILE_CREATED_EVENT = 4;
  public final static int SETTINGS_CHANGED_EVENT = 5;

  /**
   * The key of the preference where the selected profile is stored.
   */
  private final String fProfileKey;

  /**
   * The key of the preference where the version of the current settings is
   * stored
   */
  private final String fProfileVersionKey;

  /**
   * A map containing the available profiles, using the IDs as keys.
   */
  private final HashMap<String, PHPProfile> managedProfiles;

  /**
   * The available profiles, sorted by name.
   */
  private final List<PHPProfile> fProfilesByName;

  /**
   * The currently selected profile.
   */
  private PHPProfile selectedProfile;

  /**
   * The keys of the options to be saved with each profile
   */
  private final KeySet[] fKeySets;

  private final PreferencesAccess fPreferencesAccess;
  private final PHPProfileVersioner fProfileVersioner;

  /**
   * <p>
   * Create and initialize a new profile manager.
   * </p>
   *
   * @param profiles initial profiles that are managed by this manager.
   * @param context the context.
   * @param preferencesAccess the preference access.
   * @param profileVersioner the PHP profile versioner.
   */
  public PHPProfileManager(final List<PHPProfile> profiles,
      final IScopeContext context, final PreferencesAccess preferencesAccess,
      final PHPProfileVersioner profileVersioner, final KeySet[] keySets,
      final String profileKey, final String profileVersionKey) {

    fPreferencesAccess = preferencesAccess;
    fProfileVersioner = profileVersioner;
    fKeySets = keySets;
    fProfileKey = profileKey;
    fProfileVersionKey = profileVersionKey;

    managedProfiles = new HashMap<String, PHPProfile>();
    fProfilesByName = new ArrayList<PHPProfile>();

    for (final Iterator iter = profiles.iterator(); iter.hasNext();) {
      final PHPProfile profile = (PHPProfile) iter.next();
      if (profile instanceof CustomProfile) {
        ((CustomProfile) profile).setManager(this);
      }
      managedProfiles.put(profile.getID(), profile);
      fProfilesByName.add(profile);
    }

    Collections.sort(fProfilesByName);

    String profileId = getSelectedProfileId(fPreferencesAccess
        .getInstanceScope());

    PHPProfile profile = (PHPProfile) managedProfiles.get(profileId);
    if (profile == null) {
      profile = getDefaultProfile();
    }
    selectedProfile = profile;

    if (context.getName() == ProjectScope.SCOPE
        && hasProjectSpecificSettings(context)) {
      Map map = readFromPreferenceStore(context, profile);
      if (map != null) {

        List allKeys = new ArrayList();
        for (int i = 0; i < fKeySets.length; i++) {
          allKeys.addAll(fKeySets[i].getKeys());
        }
        Collections.sort(allKeys);

        PHPProfile matching = null;

        String projProfileId = context.getNode(PHPUiHelper.UI_PLUGIN_ID)
            .get(fProfileKey, null);
        if (projProfileId != null) {
          PHPProfile curr = (PHPProfile) managedProfiles.get(projProfileId);
          if (curr != null
              && (curr.isBuiltInProfile() || curr
                  .hasEqualSettings(map, allKeys))) {
            matching = curr;
          }
        }
        else {
          // old version: look for similar
          for (final Iterator iter = fProfilesByName.iterator(); iter.hasNext();) {
            PHPProfile curr = (PHPProfile) iter.next();
            if (curr.hasEqualSettings(map, allKeys)) {
              matching = curr;
              break;
            }
          }
        }
        if (matching == null) {
          String name;
          if (projProfileId != null
              && !managedProfiles.containsKey(projProfileId)) {
            name = Messages
                .format(PHPFormatterMessages.ProfileManager_unmanaged_profile_with_name,
                    projProfileId.substring(PHPProfile.ID_PREFIX.length()));
          }
          else {
            name = PHPFormatterMessages.ProfileManager_unmanaged_profile;
          }
          // current settings do not correspond to any profile -> create a 'team' profile
          SharedProfile shared = new SharedProfile(name, map, fProfileVersioner
              .getCurrentVersion(), fProfileVersioner.getProfileKind());
          shared.setManager(this);
          managedProfiles.put(shared.getID(), shared);
          fProfilesByName.add(shared); // add last
          matching = shared;
        }
        selectedProfile = matching;
      }
    }
  }

  protected String getSelectedProfileId(IScopeContext instanceScope) {
    String profileId = instanceScope.getNode(PHPUiHelper.UI_PLUGIN_ID)
        .get(fProfileKey, null);
    if (profileId == null) {
      // request from bug 129427
      profileId = new DefaultScope().getNode(PHPUiHelper.UI_PLUGIN_ID)
          .get(fProfileKey, null);
    }
    return profileId;
  }

  /**
   * Notify observers with a message. The message must be one of the following:
   *
   * @param message Message to send out
   * @see #SELECTION_CHANGED_EVENT
   * @see #PROFILE_DELETED_EVENT
   * @see #PROFILE_RENAMED_EVENT
   * @see #PROFILE_CREATED_EVENT
   * @see #SETTINGS_CHANGED_EVENT
   */
  protected void notifyObservers(int message) {
    setChanged();
    notifyObservers(new Integer(message));
  }

  public static boolean hasProjectSpecificSettings(IScopeContext context,
      KeySet[] keySets) {
    for (int i = 0; i < keySets.length; i++) {
      KeySet keySet = keySets[i];
      IEclipsePreferences preferences = context.getNode(keySet.getNodeName());
      for (final Iterator keyIter = keySet.getKeys().iterator(); keyIter
          .hasNext();) {
        final String key = (String) keyIter.next();
        Object val = preferences.get(key, null);
        if (val != null) {
          return true;
        }
      }
    }
    return false;
  }

  public boolean hasProjectSpecificSettings(IScopeContext context) {
    return hasProjectSpecificSettings(context, fKeySets);
  }

  /**
   * Only to read project specific settings to find out to what profile it
   * matches.
   *
   * @param context The project context
   */
  private Map readFromPreferenceStore(IScopeContext context,
      PHPProfile workspaceProfile) {
    final Map profileOptions = new HashMap();
    IEclipsePreferences uiPrefs = context.getNode(PHPUiHelper.UI_PLUGIN_ID);

    int version = uiPrefs.getInt(fProfileVersionKey, fProfileVersioner
        .getFirstVersion());
    if (version != fProfileVersioner.getCurrentVersion()) {
      Map allOptions = new HashMap();
      for (int i = 0; i < fKeySets.length; i++) {
        addAll(context.getNode(fKeySets[i].getNodeName()), allOptions);
      }
      CustomProfile profile = new CustomProfile(
          "tmp", allOptions, version, fProfileVersioner.getProfileKind()); //$NON-NLS-1$
      fProfileVersioner.update(profile);
      return profile.getSettings();
    }

    boolean hasValues = false;
    for (int i = 0; i < fKeySets.length; i++) {
      KeySet keySet = fKeySets[i];
      IEclipsePreferences preferences = context.getNode(keySet.getNodeName());
      for (final Iterator keyIter = keySet.getKeys().iterator(); keyIter
          .hasNext();) {
        final String key = (String) keyIter.next();
        Object val = preferences.get(key, null);
        if (val != null) {
          hasValues = true;
        }
        else {
          val = workspaceProfile.getSettings().get(key);
        }
        profileOptions.put(key, val);
      }
    }

    if (!hasValues) {
      return null;
    }
    return profileOptions;
  }

  /**
   * @param uiPrefs
   * @param allOptions
   */
  private void addAll(IEclipsePreferences uiPrefs, Map allOptions) {
    try {
      String[] keys = uiPrefs.keys();
      for (int i = 0; i < keys.length; i++) {
        String key = keys[i];
        String val = uiPrefs.get(key, null);
        if (val != null) {
          allOptions.put(key, val);
        }
      }
    }
    catch (BackingStoreException e) {
      // ignore
    }

  }

  private boolean updatePreferences(IEclipsePreferences prefs, List keys,
      Map profileOptions) {
    boolean hasChanges = false;
    for (final Iterator keyIter = keys.iterator(); keyIter.hasNext();) {
      final String key = (String) keyIter.next();
      final String oldVal = prefs.get(key, null);
      final String val = (String) profileOptions.get(key);
      if (val == null) {
        if (oldVal != null) {
          prefs.remove(key);
          hasChanges = true;
        }
      }
      else if (!val.equals(oldVal)) {
        prefs.put(key, val);
        hasChanges = true;
      }
    }
    return hasChanges;
  }

  /**
   * Update all formatter settings with the settings of the specified profile.
   *
   * @param profile The profile to write to the preference store
   */
  private void writeToPreferenceStore(PHPProfile profile, IScopeContext context) {
    final Map profileOptions = profile.getSettings();

    for (int i = 0; i < fKeySets.length; i++) {
      updatePreferences(context.getNode(fKeySets[i].getNodeName()), fKeySets[i]
          .getKeys(), profileOptions);
    }

    final IEclipsePreferences uiPrefs = context
        .getNode(PHPUiHelper.UI_PLUGIN_ID);
    if (uiPrefs.getInt(fProfileVersionKey, 0) != fProfileVersioner
        .getCurrentVersion()) {
      uiPrefs.putInt(fProfileVersionKey, fProfileVersioner.getCurrentVersion());
    }

    if (context.getName() == InstanceScope.SCOPE) {
      uiPrefs.put(fProfileKey, profile.getID());
    }
    else if (context.getName() == ProjectScope.SCOPE
        && !profile.isSharedProfile()) {
      uiPrefs.put(fProfileKey, profile.getID());
    }
  }

  /**
   * Get an immutable list as view on all profiles, sorted alphabetically.
   * Unless the set of profiles has been modified between the two calls, the
   * sequence is guaranteed to correspond to the one returned by
   * <code>getSortedNames</code>.
   *
   * @return a list of elements of type <code>Profile</code>
   * @see #getSortedDisplayNames()
   */
  public List getSortedProfiles() {
    return Collections.unmodifiableList(fProfilesByName);
  }

  /**
   * Get the names of all profiles stored in this profile manager, sorted
   * alphabetically. Unless the set of profiles has been modified between the
   * two calls, the sequence is guaranteed to correspond to the one returned by
   * <code>getSortedProfiles</code>.
   *
   * @return All names, sorted alphabetically
   * @see #getSortedProfiles()
   */
  public String[] getSortedDisplayNames() {
    final String[] sortedNames = new String[fProfilesByName.size()];
    int i = 0;
    for (final Iterator iter = fProfilesByName.iterator(); iter.hasNext();) {
      PHPProfile curr = (PHPProfile) iter.next();
      sortedNames[i++] = curr.getName();
    }
    return sortedNames;
  }

  /**
   * <p>
   * Get, and return the {@link PHPProfile} that matches this profile id.
   * </p>
   *
   * @param id The profile ID
   * @return The profile with the given ID or <code>null</code>
   */
  public PHPProfile getProfile(final String id) {
    return (PHPProfile) managedProfiles.get(id);
  }

  /**
   * Activate the selected profile, update all necessary options in preferences
   * and save profiles to disk.
   */
  public void commitChanges(IScopeContext scopeContext) {
    if (selectedProfile != null) {
      writeToPreferenceStore(selectedProfile, scopeContext);
    }
  }

  public void clearAllSettings(IScopeContext context) {
    for (int i = 0; i < fKeySets.length; i++) {
      updatePreferences(context.getNode(fKeySets[i].getNodeName()), fKeySets[i]
          .getKeys(), Collections.EMPTY_MAP);
    }

    final IEclipsePreferences uiPrefs = context
        .getNode(PHPUiHelper.UI_PLUGIN_ID);
    uiPrefs.remove(fProfileKey);
  }

  /**
   * Get the currently selected profile.
   *
   * @return The currently selected profile.
   */
  public IProfile getSelected() {
    return selectedProfile;
  }

  /**
   * Set the selected profile. The profile must already be contained in this
   * profile manager.
   *
   * @param profile The profile to select
   */
  public void setSelected(PHPProfile profile) {
    final PHPProfile newSelected = (PHPProfile) managedProfiles.get(profile.getID());
    if (newSelected != null && !newSelected.equals(selectedProfile)) {
      selectedProfile = newSelected;
      notifyObservers(SELECTION_CHANGED_EVENT);
    }
  }

  /**
   * Check whether a user-defined profile in this profile manager already has
   * this name.
   *
   * @param name The name to test for
   * @return Returns <code>true</code> if a profile with the given name exists
   */
  public boolean containsName(String name) {
    for (final Iterator iter = fProfilesByName.iterator(); iter.hasNext();) {
      PHPProfile curr = (PHPProfile) iter.next();
      if (name.equals(curr.getName())) {
        return true;
      }
    }
    return false;
  }

  /**
   * Add a new custom profile to this profile manager.
   *
   * @param profile The profile to add
   */
  public void addProfile(final CustomProfile profile) {
    profile.setManager(this);
    final CustomProfile oldProfile = (CustomProfile) managedProfiles
        .get(profile.getID());
    if (oldProfile != null) {
      managedProfiles.remove(oldProfile.getID());
      fProfilesByName.remove(oldProfile);
      oldProfile.setManager(null);
    }
    managedProfiles.put(profile.getID(), profile);
    fProfilesByName.add(profile);
    Collections.sort(fProfilesByName);
    selectedProfile = profile;
    notifyObservers(PROFILE_CREATED_EVENT);
  }

  /**
   * Delete the currently selected profile from this profile manager. The next
   * profile in the list is selected.
   *
   * @return true if the profile has been successfully removed, false otherwise.
   */
  public boolean deleteSelected() {
    if (!(selectedProfile instanceof CustomProfile))
      return false;

    return deleteProfile((CustomProfile) selectedProfile);
  }

  public boolean deleteProfile(CustomProfile profile) {
    int index = fProfilesByName.indexOf(profile);

    managedProfiles.remove(profile.getID());
    fProfilesByName.remove(profile);

    profile.setManager(null);

    if (index >= fProfilesByName.size())
      index--;
    selectedProfile = (PHPProfile) fProfilesByName.get(index);

    if (!profile.isSharedProfile()) {
      updateProfilesWithName(profile.getID(), null, false);
    }

    notifyObservers(PROFILE_DELETED_EVENT);
    return true;
  }

  public void profileRenamed(CustomProfile profile, String oldID) {
    managedProfiles.remove(oldID);
    managedProfiles.put(profile.getID(), profile);

    if (!profile.isSharedProfile()) {
      updateProfilesWithName(oldID, profile, false);
    }

    Collections.sort(fProfilesByName);
    notifyObservers(PROFILE_RENAMED_EVENT);
  }

  public void profileReplaced(CustomProfile oldProfile, CustomProfile newProfile) {
    managedProfiles.remove(oldProfile.getID());
    managedProfiles.put(newProfile.getID(), newProfile);
    fProfilesByName.remove(oldProfile);
    fProfilesByName.add(newProfile);
    Collections.sort(fProfilesByName);

    if (!oldProfile.isSharedProfile()) {
      updateProfilesWithName(oldProfile.getID(), null, false);
    }

    setSelected(newProfile);
    notifyObservers(PROFILE_CREATED_EVENT);
    notifyObservers(SELECTION_CHANGED_EVENT);
  }

  public void profileChanged(CustomProfile profile) {
    if (!profile.isSharedProfile()) {
      updateProfilesWithName(profile.getID(), profile, true);
    }

    notifyObservers(SETTINGS_CHANGED_EVENT);
  }

  /**
   * @param oldName the old name of the profile.
   * @param newProfile the new profile.
   * @param applySettings whether or not to apply the settings.
   */
  protected void updateProfilesWithName(final String oldName,
      final PHPProfile newProfile, final boolean applySettings) {
    IProject[] projects = ResourcesPlugin.getWorkspace().getRoot()
        .getProjects();
    for (int i = 0; i < projects.length; i++) {
      IScopeContext projectScope = fPreferencesAccess
          .getProjectScope(projects[i]);
      IEclipsePreferences node = projectScope.getNode(PHPUiHelper.UI_PLUGIN_ID);
      String profileId = node.get(fProfileKey, null);
      if (oldName.equals(profileId)) {
        if (newProfile == null) {
          node.remove(fProfileKey);
        }
        else {
          if (applySettings) {
            writeToPreferenceStore(newProfile, projectScope);
          }
          else {
            node.put(fProfileKey, newProfile.getID());
          }
        }
      }
    }

    IScopeContext instanceScope = fPreferencesAccess.getInstanceScope();
    final IEclipsePreferences uiPrefs = instanceScope
        .getNode(PHPUiHelper.UI_PLUGIN_ID);
    if (newProfile != null && oldName.equals(uiPrefs.get(fProfileKey, null))) {
      writeToPreferenceStore(newProfile, instanceScope);
    }
  }

  /**
   * @return
   */
  public abstract PHPProfile getDefaultProfile();

  /**
   * @return
   */
  public PHPProfileVersioner getProfileVersioner() {
    return fProfileVersioner;
  }
}
